using System.Collections.Generic;
using System.Linq;
using BSDS.Core.Data.Local;
using BSDS.Core.Model.Services.Geocoding;
using BSDS.Core.Model.Validation;
using System;

namespace BSDS.Core.Model.Data
{
    public class BsdsData
    {
        public EntityValue[] Entities { get; private set; }
        public DataFormat OriginalFormat { get; private set; }
        public string OriginalFilename { get; private set; }
        public string EntityTypeName { get; set; }

        public BsdsData(EntityValue[] entities, string orignialFilename, DataFormat orignialFormat, string entityTypeName)
        {
            EntityTypeName = entityTypeName;
            Entities = entities;
            OriginalFilename = orignialFilename;
            OriginalFormat = orignialFormat;
        }

        public ValidationResult Validate()
        {
            return new BsdsDataValidator(this).Validate();
        }

        public void UpdateReverseGeocoding(EntityValue[] source)
        {
            var propertyNames = new[] {
                "AddressLine", 
                "AdminDistrict", 
                "CountryRegion",
                "District", 
                "FormattedAddress", 
                "Locality", 
                "PostalCode",
                "PostalTown", 
                "DisplayName", 
                "EntityType"
            };
     
            UpdateProperties(source, propertyNames);
        }

        public void UpdateGeocoding(EntityValue[] source)
        {
            var propertyNames = new[] {
                "Latitude", 
                "Longitude", 
                "Confidence", 
                "MatchCode", 
                "MatchedMethod", 
                "EntityType", 
                "FormattedAddress",
                "Geocoder"
            };

            UpdateProperties(source, propertyNames);
        }

        /// <summary>
        /// Designed to detect lat/lon edits made through the Bing Maps Portal. 
        /// Requires MatchedMethod and Confidence columns in the dataset.
        /// </summary>
        public void FixMatchedMethod()
        {
            if (!Entities[0].AttributeInfos.Any(x => x.Name == "MatchedMethod"))
                return;
            if (!Entities[0].AttributeInfos.Any(x => x.Name == "Confidence"))
                return;

            foreach (var entity in Entities)
            {
                var mmValue = entity.GetAttributeValue("MatchedMethod");
                var confidenceValue = entity.GetAttributeValue("Confidence");

                if (confidenceValue == "BadRequest")
                    continue;
                
                if (mmValue.Trim() == string.Empty || mmValue.ToLower().Contains("manual") ||
                    confidenceValue == string.Empty)
                {
                    entity.UpdateAttributeValue("MatchedMethod", "Manual");
                }
            }
        }

        /// <summary>
        /// When the same address appears more than once in a dataset, ensure that each of them are assigned the
        /// same lat/lon values. Also, when the Confidence column defined in the dataset, its value will be used 
        /// determine which geocode is the most precise if a difference in quality is found for the same address 
        /// in the dataset. This feature is added because Bing uses multiple geocoders and each can return
        /// slightly different results for the same address.
        /// </summary>
        public void fixInconsistentGeocodes()
        {
            // Make sure the dataset includes the "Confidence" column, otherwise, we
            bool ignoreConfidence = !Entities[0].AttributeInfos.Any(x => x.Name == "Confidence");

            var geoConfidenceDictionary = buildGeoconfidenceDictionary(ignoreConfidence);
            GeoConfidenceCoordinate gcc = null;

            // Update all 
            foreach (var entity in Entities)
            {
                string addressKey = BuildAddressKey(entity);

                if (geoConfidenceDictionary.TryGetValue(addressKey, out gcc))
                {
                    entity.UpdateAttributeValue("Latitude", gcc.lat);
                    entity.UpdateAttributeValue("Longitude", gcc.lon);
                    entity.UpdateAttributeValue("Confidence", gcc.confidence.ToString());
                }
            }
        }

        private Dictionary<string, GeoConfidenceCoordinate> buildGeoconfidenceDictionary(bool ignoreConfidence)
        {
            var geoConfidenceDictionary = new Dictionary<string, GeoConfidenceCoordinate>();
            GeoConfidenceCoordinate gcc = null;

            // Build a dictionary of all unique addresses and corresponding lat/lon values. For collisions, better geocode wins
            foreach (var entity in Entities)
            {
                // Build address key from city,state,zip
                string addressKey = BuildAddressKey(entity);

                // Get the geocode confidence level
                string confidenceString;
                GEOCONFIDENCE? confidence = null;

                if (!ignoreConfidence)
                {
                    confidenceString = entity.GetAttributeValue("Confidence");
                    confidence = GeoConfidenceCoordinate.ConvertToGeoconfidence(confidenceString);
                }

                // Get lat/lon
                string lat = entity.GetAttributeValue("Latitude");
                string lon = entity.GetAttributeValue("Longitude");

                // If address is not already in dictionary, then add it
                if (!geoConfidenceDictionary.TryGetValue(addressKey, out gcc))
                {
                    geoConfidenceDictionary.Add(addressKey, new GeoConfidenceCoordinate() { lat = lat, lon = lon, confidence = confidence } );
                }
                // Otherwise, replace it only if the confidence value is higher (rare, but possible with the Bing geocoders as the better geocoder may time out on occasion)
                else
                {
                    // Special case: if dataset does not include confidence column, keep the current value
                    // TODO: Check MatchedMethod. If it is "Manual" then then use it as the highest priorty.
                    if (confidence != null && confidence > gcc.confidence)
                    {
                        gcc.confidence = confidence;
                        gcc.lat = lat;
                        gcc.lon = lon;
                        //addressDictionary.Remove(addressKey);
                        //addressDictionary.Add(addressKey, new GeoConfidenceCoordinate() { lat = lat, lon = lon, confidence = confidence } );
                    }
                }
            }

            return geoConfidenceDictionary;
        }

        private string BuildAddressKey(EntityValue entity)
        {
                        // Build address key from city,state,zip
           string addr = entity.GetAttributeValue("AddressLine");
           string city = entity.GetAttributeValue("Locality");
           string state = entity.GetAttributeValue("AdminDistrict");
           string addressKey = addr.Trim().ToLower() + "," + city.Trim().ToLower() + "," + state.Trim().ToLower();

           return addressKey;
        }


        //public static int BinarySearchForMatch<T>(this IList<T> list, Func<T,int> comparer)
        //{
        //    int min = 0;
        //    int max = list.Count-1;
        //
        //    while (min <= max)
        //    {
        //        int mid = (min + max) / 2;
        //        int comparison = comparer(list[mid]);
        //        if (comparison == 0)
        //        {
        //            return mid;
        //        }
        //        if (comparison < 0)
        //        {
        //            min = mid+1;
        //        }
        //        else
        //        {
        //            max = mid-1;
        //        }
        //    }
        //    return ~min;
        //}
        
        public void ZeroOutEmptyLatitudeLongitudeEntities()
        {
            foreach (var entity in Entities)
            {
                if (entity.GetAttributeValue("Latitude") == string.Empty)
                    entity.UpdateAttributeValue("Latitude", "0.0");
                if (entity.GetAttributeValue("Longitude") == string.Empty)
                    entity.UpdateAttributeValue("Longitude", "0.0");
            }
        }

        public void RemoveColumn(string column)
        {
            foreach (var entity in Entities)
            {
                entity.RemoveAttributeValue(column);
            }
        }

        public EntityValue[] GetNotReadyEntities(GeocodingMode geocodingMode)
        {
            if (geocodingMode == GeocodingMode.Empty)
            {
                return Entities.Where(e => !e.HasValidCoordinates()).ToArray();
            }
            else if (geocodingMode == GeocodingMode.Reverse)
            {
                return Entities.Where(e => e.HasValidCoordinates()).ToArray();
            }
            else
                return Entities;
        }

        public EntityValue[] GetImpreciseOrBadEntities()
        {
            return Entities.Where(e => e.IsImpreciseOrBadEntity()).ToArray();
        }

        public bool ContainsEntity(string EntityID)
        {
            return Entities.Any(e => e.PrimaryKeyValue == EntityID);
        }

        public EntityValue[] GetMatchingEntities(GeocodingResult selectedEntities)
        {
            return Entities.Where(e => selectedEntities.ContainsEntity(e.PrimaryKeyValue)).ToArray();
        }

        public bool GeocodingNeeded(GeocodingMode geocodingMode)
        {
            if (geocodingMode == GeocodingMode.Empty)
            {
                return Entities.Any(e => !e.HasValidCoordinates());
            }

            return true;
        }

        private void UpdateProperties(IEnumerable<EntityValue> source, string[] propertyNames)
        {
            //TODO: filter out unsuccessful results
            //geoData = FilterOutUnsuccessfulResults(geoData);

            var originalDataIndex = Entities.ToDictionary(e => e.PrimaryKeyValue, e => e); 
            var sourceDataIndex = source.ToDictionary(e => e.PrimaryKeyValue, e => e);

            foreach (var sourceKey in sourceDataIndex.Keys)
            {
                if (originalDataIndex.ContainsKey(sourceKey))
                {
                    var sourceDataEntity = sourceDataIndex[sourceKey];
                    var originalDataEntity = originalDataIndex[sourceKey];

                    foreach (var propertyName in propertyNames)
                    {
                        var p = originalDataEntity.AttributeValues.FirstOrDefault(a => a.AttributeInfo.Name == propertyName);
                        if (p == null)
                            continue;

                        originalDataEntity.UpdateAttributeFromOtherEntity(sourceDataEntity, propertyName);
                    }
                }
            }
        }
    }
 }
